﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace DeflateLib
{
    public class DeflateHintCreator
    {
        const int DefaultHintInterval = 0x10000;
        const int MinHintInterval = 0x8000;

        internal const int HeaderLength = 0x20;
        internal static byte[] Signature = { (byte)'I', (byte)'x', (byte)'D', (byte)'1' };

        int hintInterval = DefaultHintInterval;

        public int HintInterval
        {
            get { return hintInterval; }
            set
            {
                if (value < MinHintInterval) throw new ArgumentOutOfRangeException();
                hintInterval = value;
            }
        }

        public void Process(Stream deflateStream, Stream hintStream)
        {
            long streamOffset = deflateStream.Position;

            long hintStreamOffset = hintStream.Position;

            WriteHeader(hintStream);

            DecoderWithDependenciesOutput decoder = new DecoderWithDependenciesOutput(deflateStream);
            long hintPosition = 0;
            List<long> hintDataOffsets = new List<long>();
            byte[] data;
            do
            {
                DecoderWithDependenciesOutput.DecompressionState state = decoder.GetDecompressionState();
                DecoderWithDependenciesOutput.PreviousDataDependency[] dependencies;
                decoder.ReadExact(HintInterval, out data, out dependencies);

                long hintStreamPosition = hintStream.Position;
                hintDataOffsets.Add(hintStreamPosition);

                WriteHintData(hintStream, state, data, dependencies);

                hintPosition += HintInterval;
            } while (data.Length == HintInterval);

            AlignHintStream(hintStream);

            long hintIndexStart = hintStream.Position;
            WriteHintIndex(hintStream, hintDataOffsets);

            UpdateHeader(hintStream, hintStreamOffset, hintIndexStart, streamOffset, hintPosition);
        }

        private void UpdateHeader(Stream hintStream, long hintStreamOffset,
            long indexPosition, long streamOffset, long uncompressedSize)
        {
            byte[] buf;

            byte[] header = new byte[HeaderLength];
            Array.Copy(Signature, 0, header, 0, Signature.Length);

            buf = BitConverter.GetBytes(HintInterval);
            Array.Copy(buf, 0, header, 4, buf.Length);
            buf = BitConverter.GetBytes(indexPosition);
            Array.Copy(buf, 0, header, 8, buf.Length);
            buf = BitConverter.GetBytes(uncompressedSize);
            Array.Copy(buf, 0, header, 16, buf.Length);
            buf = BitConverter.GetBytes(streamOffset);
            Array.Copy(buf, 0, header, 24, buf.Length);

            long savedPostion = hintStream.Position;
            hintStream.Position = hintStreamOffset;
            hintStream.Write(header, 0, header.Length);
            hintStream.Position = savedPostion;
        }

        private void WriteHintIndex(Stream hintStream, List<long> hintDataOffsets)
        {
            byte[] buf;
            for (int i = 0; i < hintDataOffsets.Count; i++)
            {
                buf = BitConverter.GetBytes(hintDataOffsets[i]);
                hintStream.Write(buf, 0, buf.Length);
            }

        }

        private void AlignHintStream(Stream hintStream)
        {
            long indexPosition = hintStream.Position;
            int alignBy = (int)(8 - (indexPosition & 7));
            if (alignBy < 8)
            {
                hintStream.Write(new byte[alignBy], 0, alignBy);
                indexPosition += alignBy;
            }
        }

        private void WriteHintData(Stream hintStream,
            DecoderWithDependenciesOutput.DecompressionState state, 
            byte[] readData, 
            DecoderWithDependenciesOutput.PreviousDataDependency[] dependencies)
        {
            byte flags = 0;
            flags |= (byte)(state.BlockType & 0x03);
            if (state.BlockType == 0 && state.Block0Delayed < HintInterval)
            {
                flags |= 0x04;
            }

            if (state.IsBlockFinal)
                flags |= 0x80;

            const int MaxDataSize = 0x8000;
            bool[] currentDataMask = new bool[MaxDataSize];
            byte[] currentData = new byte[MaxDataSize];
            int atomOffset = state.Block12Delayed;
            if (dependencies != null)
            {
                for (int i = 0; i < dependencies.Length; i++)
                {
                    int dataToCopyLength = dependencies[i].Length;
                    if (dataToCopyLength > dependencies[i].Offset)
                    {
                        dataToCopyLength = dependencies[i].Offset;
                    }

                    int dataToCopyPosition = currentData.Length - atomOffset - dependencies[i].Offset;
                    int dataToCopyLeft = dataToCopyLength;
                    int blockSize = dependencies[i].Data.Length;
                    while (dataToCopyLeft > blockSize)
                    {
                        Array.Copy(dependencies[i].Data, 0, currentData, dataToCopyPosition, blockSize);
                        dataToCopyLeft -= blockSize;
                        dataToCopyPosition += blockSize;
                    }
                    Array.Copy(dependencies[i].Data, 0, currentData, dataToCopyPosition, dataToCopyLeft);

                    for (int j = 0; j < dataToCopyLength; j++)
                    {
                        currentDataMask[currentData.Length - atomOffset - dependencies[i].Offset + j] = true;
                    }
                }
            }
            if (atomOffset > 0)
            {
                Array.Copy(readData, 0, currentData, currentData.Length - atomOffset, atomOffset);
                for (int j = 0; j < atomOffset; j++)
                {
                    currentDataMask[currentData.Length - atomOffset + j] = true;
                }
            }

            int startIndex = 0;
            while (startIndex < currentDataMask.Length && !currentDataMask[startIndex])
            {
                startIndex++;
            }

            if (startIndex < currentDataMask.Length)
            {
                flags |= 0x08;
            }

            if (atomOffset > 0)
            {
                flags |= 0x10;
            }

            hintStream.WriteByte(flags);
            WriteBytes(hintStream, state.DataBitPosition);
            if (state.BlockType == 2)
            {
                WriteBytes(hintStream, state.BlockBitPosition + 3);
            }
            if (state.BlockType == 0 && state.Block0Delayed < HintInterval)
            {
                WriteBytes(hintStream, state.Block0Delayed);
            }

            if (startIndex < currentDataMask.Length)
            {
                if (atomOffset > 0)
                {
                    WriteBytes(hintStream, atomOffset);
                }

                List<byte> unpacked = new List<byte>();
                List<int> intervals = new List<int>();
                int i = startIndex;
                int j = startIndex;
                while (i < currentDataMask.Length && currentDataMask[i])
                {
                    unpacked.Add(currentData[i]);
                    i++;
                }
                intervals.Add(i - j);
                while (i < currentDataMask.Length)
                {
                    j = i;
                    while (i < currentDataMask.Length && !currentDataMask[i])
                    {
                        i++;
                    }
                    intervals.Add(i - j);

                    if (i >= currentDataMask.Length) break;

                    j = i;
                    while (i < currentDataMask.Length && currentDataMask[i])
                    {
                        unpacked.Add(currentData[i]);
                        i++;
                    }
                    intervals.Add(i - j);
                }

                WriteBytes(hintStream, intervals.Count);
                for (int q = 0; q < intervals.Count; q++)
                {
                    WriteBytes(hintStream, intervals[q]);
                }

                byte[] packed = PackData(unpacked.ToArray());
                hintStream.Write(packed, 0, packed.Length);
            }
        }

        private void WriteHeader(Stream hintStream)
        {
            hintStream.Write(new byte[HeaderLength], 0, HeaderLength);
        }

        private static byte[] PackData(byte[] unpacked)
        {
            System.IO.MemoryStream ms = new System.IO.MemoryStream();
            Encoder encoder = new Encoder(ms);
            encoder.Encode(unpacked, 0, unpacked.Length, true);
            return ms.ToArray();
        }

        static void WriteBytes(Stream target, long n)
        {
            byte[] buf = ToBytes(n);
            target.Write(buf, 0, buf.Length);
        }

        static byte[] ToBytes(long n)
        {
            List<byte> b = new List<byte>();
            while (n >= 0x80)
            {
                b.Add((byte)((n & 0x7F) | 0x80));
                n >>= 7;
            }
            b.Add((byte)n);
            return b.ToArray();
        }

    }
}
